Skip to content

feat(privacy): fixed MeshView-specific notice, replaces configurable text model - #14

Open
dborup wants to merge 4 commits into
masterfrom
feature/remove-privacy-rights-section
Open

feat(privacy): fixed MeshView-specific notice, replaces configurable text model#14
dborup wants to merge 4 commits into
masterfrom
feature/remove-privacy-rights-section

Conversation

@dborup

@dborup dborup commented Sep 3, 2026

Copy link
Copy Markdown
Owner

What this changes

The #/privacy page's notice text is now the operator's (meshview.dk's) fixed, hardcoded content, not an operator-configurable template:

  • public/privacy.js carries the approved notice verbatim, as a typed run structure, rendered through a closed set of tags. It is MeshView-specific content, not a general-purpose privacy-policy generator.
  • This replaces the earlier operator-configurable, 17-field, Validate()-gated text model shipped in feat(privacy): opt-in config-driven GDPR privacy-notice page (#/privacy) #12. That model is gone: PrivacyConfig and PrivacyClientConfig now carry exactly one field.
  • config.json's privacy block controls only privacy.enabled — nothing else. No config value can add, remove, or reword a line of the notice.
  • The MeshCore link in the notice points at https://meshcore.io.

Independent review

This branch went through an independent, read-only review by a separate agent session (no shared authorship with the implementation), covering the full commit series against baseline 651be3e0:

  • Structural contract verified: exactly 7 headings, 4 list items, 1 link, single-flag API payload ({"enabled":true} when on, field omitted entirely when off/unconfigured), legacy config files with the old fields still load safely (values ignored, never published), no dangling references to the removed config model anywhere in the codebase.
  • Adversarial checks: hostile/legacy config values cannot inject markup or reach the page; direct #/privacy access while disabled shows a neutral "not published" state; nav injection (drawer/More-sheet/desktop) has no duplicates and reconciles correctly regardless of config-arrival timing.
  • A final independent word-for-word text comparison was run directly against the site owner's authoritative text (supplied outside the repo, not against the implementation's own test fixture). Result: zero remaining deviations. One deviation found during that check (the MeshCore link pointing at the old meshcore.co.uk domain) was corrected in this branch's final commit.

This is a technical/structural/security verification, not a legal GDPR compliance approval. It confirms the code does what it claims to do and matches the text the operator approved; it does not assess the legal adequacy of that text.

Known gap

Staging verification of this exact HEAD is still pending — the independent review ran the full test suite and a local (non-network) render harness, but has not yet been verified against a live staging deployment.

Test results

  • test-privacy-page.js: 34/34 passing
  • Go cmd/server package: full suite passing (go build, go vet, go test ./... all clean)
  • node --check, git diff --check: clean

🤖 Generated with Claude Code

dborup and others added 4 commits September 2, 2026 18:35
The section is removed outright rather than hidden behind a config flag:
there is no key that can bring it back. Gone from the rendered page are the
"Your rights" heading, the access/rectification/erasure/restriction/
portability/objection boilerplate, the "requests are assessed" wording, the
node hide-or-remove offer, the GDPR Art. 15-21 reference, the public-channel
note, "Send privacy requests to <contact>" and the whole complaint block
naming the supervisory authority and linking to it.

section() emits only an <h3> plus its body, so deleting the statement takes
the heading with it -- no empty wrapper, section or separator is left. The
page now runs "Hidden nodes" straight into "Automated decision-making".

Three config fields fed that section and nothing else, so they are removed
consistently across the stack:

  rightsRequestText, supervisoryAuthorityName, supervisoryAuthorityUrl

  - PrivacyConfig struct fields and their Validate() entries (config.go),
    including the dedicated supervisoryAuthorityUrl scheme check
  - PrivacyClientConfig DTO (types.go) and the /api/config/client publish
    block (routes.go), so they no longer ship to any browser
  - public/privacy.js reads
  - config.example.json keys and their _comment_ documentation
  - Go and Node test fixtures, required-field tables and assertions

Two helpers died with them and are removed too: safeUrl() in privacy.js and
isSafeHTTPURL() in config.go, each of which had exactly one call site in the
deleted code. Dropping isSafeHTTPURL also drops the now-unused net/url
import.

contactEmail is KEPT: it is rendered outside the section, in the page header
("Privacy contact:"), and still validated by privacyEmailRe.

A config.json written before this change still carries the three keys. The
decoder does not reject unknown fields, so such a deployment keeps working:
the keys are ignored, never validated and never published.

13 regression tests added (10 Node, 3 Go) pinning that none of the removed
headings, texts or links render, that stale config values cannot resurface
them, that no empty heading or wrapper is left behind, that the fields are
absent from the published API payload, and that the contact link and every
remaining section still render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit trimmed one section out of a config-driven page. That
missed the requirement: the notice is an approved document, and the page's
visible content must be exactly that document -- no more, no less.

The page is now a FIXED document. public/privacy.js carries the notice as a
typed block/run structure (heading, paragraph, list, bold, link, hard break)
and renders it through a closed set of tags. Config decides only WHETHER the
page is published, never what it says: privacy.js reads cfg.enabled and
nothing else, so no operator value -- current, stale or newly added -- can
add, remove or reword a line.

Removed from the rendered page (static boilerplate that was never in the
approved text): the "Privacy Notice" title, effective date, data-controller
and privacy-contact header, the CoreScope intro paragraph, "What data this
site processes", the legal-basis label and its "stated by the operator"
note, "Sources of the data", "Who can receive the data", the retention
caveat, "Channel and direct messages", "Storage in your browser", "Server
and proxy logs", "External services", "International transfers", "Hidden
nodes", "Automated decision-making", "Changes to this notice" and the DPO
block.

Config fields removed, because the page no longer reads any of them and a
required field that cannot affect the page is a trap -- a blank one would
have withheld the notice entirely:

  PrivacyConfig  -> Enabled only. Gone: controllerName, contactEmail,
  effectiveDate, purposesText, legalBasisType, legalBasisText,
  legitimateInterestsText, retentionText, recipientsText, dataSourcesText,
  thirdPartyServicesText, internationalTransfersText, browserStorageText,
  serverLogsText, automatedDecisionMakingText, dpoName, dpoContact.

  PrivacyClientConfig -> Enabled only; hiddenNamePrefixes no longer rides
  along in the privacy block.

With no fields left to check, Validate(), logPrivacyConfigErrors(),
privacyEmailRe, privacyLegalBasisTypes and PrivacyLegalBasisTypes() are gone
too, and with them the regexp/sort/strconv imports. The publish gate is now
just privacy.enabled. Config.ActiveHiddenNamePrefixes() is KEPT: the privacy
page no longer consumes it, but its test pins an IsNameHidden invariant that
belongs to node hiding.

Safety: the notice is code, not config. Every text run goes through
escapeHtml; the single href is a compile-time constant re-checked against an
absolute-http(s) pattern at render time, and an unsafe URL degrades to plain
text instead of becoming a link. Nothing from window.MC_PRIVACY reaches the
DOM. A pre-removal config.json still parses and is still accepted -- its
values are simply ignored and never published.

Tests: a golden test normalises the rendered markup back to visible text and
compares the whole result against the authoritative Markdown, held verbatim
in the test (String.raw, so JS cannot eat the \. \@ escapes or the
hard-break backslash before the comparison sees them). Mutation-checked: it
fails on added text, removed text and a single reworded word. Alongside it:
exactly 7 headings in order, exactly 4 data points with their bold lead-ins,
the MeshCore URL, meshview.dk and kontakt@meshview.dk rendering without
backslashes, both square-bracket passages intact, and no extra section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mment

The comment still described the field as "operator-configured
privacy-notice content" published only once the block passes
PrivacyConfig.Validate(). Neither is true any more: the notice is a fixed
document in public/privacy.js, Validate() no longer exists, and the block is
published on privacy.enabled alone.

Comment only — no code, no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Independent review compared the rendered #/privacy notice word-for-word
against the user's authoritative text (supplied directly, not sourced from
this repo). Every heading, paragraph, list item, bold emphasis, line break
and bracketed passage matched exactly except one: the MeshCore link pointed
at https://meshcore.co.uk/, where the approved text specifies
https://meshcore.io.

Three call sites carried the old URL and are now aligned:
- public/privacy.js: the DOC link itself (the rendered href)
- test-privacy-page.js: the AUTHORITATIVE golden fixture
- test-privacy-page.js: the link-safety assertion and the
  "notice text lives in code" static-source check

No other content changed. Structure (7 headings, 4 list items, 1 link),
bold segments, the controller/contact line break, and both bracketed
passages were independently re-verified unchanged and byte-identical to
the approved text after this fix.

test-privacy-page.js: 34/34 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant